<?php
/**
 * WebUser component
 *
 * This file contains the user application component
 *
 * @author Andreev Sergey <andsermail@gmail.com>
 */

class WebUser extends CApplicationComponent implements IWebUser
{
    const FLASH_KEY_PREFIX = 'WebUser.flash.';
    const FLASH_COUNTERS = 'WebUser.flashcounters';
    const STATES_VAR = '__states';
    const SECRET_KEY = '3fba2794';
    const COOKIE_FORMAT_VERSION = '1.0';

    /**
     * @var string cookie key
     */
    public $cookieName = 'user__';

    /**
     * @var string cookie domain
     */
    public $cookieDomain = '';

    /**
     * @var string the name for a guest user. Defaults to 'Guest'.
     * This is used by {@link getName} when the current user is a guest (not authenticated).
     */
    public $guestName = 'Guest';

    /**
     * @var string|array the URL for login. If using array, the first element should be
     * the route to the login action, and the rest name-value pairs are GET parameters
     * to construct the login URL (e.g. array('/site/login')). If this property is null,
     * a 403 HTTP exception will be raised instead.
     * @see CController::createUrl
     */
    public $loginUrl = array('/site/login');

    private $_access = array();

    /**
     * @var array Unparsed effective data from cookie
     */
    private $_user_data;
    private $_keyPrefix;

    /**
     * @param  $name string Name for user data key
     * @param  $value Value for user data key
     * @return void
     */
    protected function setUserData($name, $value)
    {
        $this->_user_data[$name] = $value;
    }

    /**
     * @param  $name string Name of user data key
     * @return array|bool
     */
    protected function getUserData($name)
    {
        return (isset($this->_user_data[$name])) ? $this->_user_data[$name] : false;
    }

    /**
     * PHP magic method.
     * This method is overriden so that persistent states can be accessed like properties.
     * @param string $name property name
     * @return mixed property value
     * @since 1.0.3
     */
    public function __get($name)
    {
        if ($this->getUserData($name))
            return $this->getUserData($name);
        else
            return parent::__get($name);
    }

    /**
     * Initializes the application component.
     * This method overrides the parent implementation by performing cookie-based authentication
     */
    public function init()
    {
        parent::init();
        $cookie = $this->getCookie();
        if ($cookie !== false) {
            $this->_user_data = $cookie;
        } else {
            Yii::app()->request->cookies->remove($this->cookieName);
        }
    }

    /**
     * Returns the URL that the user should be redirected to after successful login.
     * This property is usually used by the login action. If the login is successful,
     * the action should read this property and use it to redirect the user browser.
     *
     * @return string the URL that the user should be redirected to after login.
     * @see loginRequired
     */
    public function getReturnUrl()
    {
        return Yii::app()->getRequest()->getScriptUrl();
    }

    /**
     * This method is called before logging in a user.
     * You may override this method to provide additional security check.
     * For example, when the login is cookie-based, you may want to verify
     * that the user ID together with a random token in the states can be found
     * in the database. This will prevent hackers from faking arbitrary
     * identity cookies even if they crack down the server private key.
     * @param mixed $id the user ID. This is the same as returned by {@link getId()}.
     * @param array $states a set of name-value pairs that are provided by the user identity.
     * @param boolean $fromCookie whether the login is based on cookie
     * @return boolean whether the user should be logged in
     */
    protected function beforeLogin($id, $states, $fromCookie)
    {
        return true;
    }

    /**
     * This method is called after the user is successfully logged in.
     * You may override this method to do some postprocessing (e.g. log the user
     * login IP and time; load the user permission information).
     * @param boolean $fromCookie whether the login is based on cookie.
     */
    protected function afterLogin($fromCookie)
    {
    }

    /**
     * This method is invoked when calling {@link logout} to log out a user.
     * If this method return false, the logout action will be cancelled.
     * You may override this method to provide additional check before
     * logging out a user.
     * @return boolean whether to log out the user
     */
    protected function beforeLogout()
    {
        return true;
    }

    /**
     * This method is invoked right after a user is logged out.
     * You may override this method to do some extra cleanup work for the user.
     */
    protected function afterLogout()
    {
    }

    /**
     * @param  $identity UserIdentity
     * @param  $duration integer
     * @return void
     */
    public function login($identity, $duration)
    {
        $user_id = $identity->getId();
        $name = $identity->getName();
        $email = $identity->getEmail();
        $authFrom = $identity->getAuthFrom();
        // @todo Implement an ability to get/set persistent states
        $states = $identity->getPersistentStates();
        if ($this->beforeLogin($user_id, $states, false)) {
            $cookie_data = array(
                'id' => $user_id,
                'name' => $name,
                'email' => $email,
                'authFrom' => $authFrom,
            );
            $this->_user_data = $cookie_data;
            $this->setCookie($cookie_data, $duration);
            $this->afterLogin(false);
        }
    }

    /**
     * Logs out the current user.
     * This will remove auth cookie
     */
    public function logout()
    {
        if ($this->beforeLogout()) {
            // This not working (when using cookiDomain):
            // Yii::app()->getRequest()->getCookies()->remove($this->cookieName);
            // But this - works perfect:
            if (version_compare(PHP_VERSION, '5.2.0', '>=')) {
                setcookie($this->cookieName, null, 0, "/", $this->cookieDomain, null, null);
            } else {
                setcookie($this->cookieName, null, 0, "/", $this->cookieDomain, null);
            }
            unset(Yii::app()->request->cookies[$this->cookieName]);
            unset($_COOKIE[$this->cookieName]);
            setcookie($this->cookieName, NULL, -1);
            $this->afterLogout();
        }
    }

    /**
     * This method validates cookie value.
     * $value must be a string
     * $value must have 4 string entities separated by '/' symbol:
     * - cookie format version;
     * - effective data
     * - timestamp of cookie creation
     * - checksum
     * Cookie version must be: 1.0
     * Timestamp must be numeric
     * Length of checksum must be 40 symbols
     * Effective data must be encoded by base64_encode function
     * Decoded effective data must be an associative array
     * Checksum must be identical to generateChecksum() function result
     * @param  $value string Cookie value
     * @return bool If cookie value is valid - return true, else - return false
     */
    public function validateCookieValue($value)
    {
        if (!is_string($value)) return false;
        $a_value = explode('/', $value);
        if (!is_array($a_value) || count($a_value) != 4) return false;
        list($ver, $data, $timestamp, $checksum) = $a_value;
        if ($ver != self::COOKIE_FORMAT_VERSION) return false;
        if (!is_numeric($timestamp)) return false;
        if (strlen($checksum) != 40) return false;
        if (!is_array(json_decode(base64_decode($data), true))) return false;
        if ($this->generateChecksum($ver . '/' . $data . '/' . $timestamp, $timestamp) !== $checksum) {
            return false;
        }
        return true;
    }

    /**
     * @param array $data User data
     * @param int $duration Cookie lifetime
     * @return CHttpCookie
     */
    protected function setCookie($data, $duration = 0)
    {
        $t = time();
        $value = self::COOKIE_FORMAT_VERSION . '/' . base64_encode(json_encode($data)) . '/' . $t;
        $final_value = $value . '/' . $this->generateChecksum($value, $t);
        $cookie = new CHttpCookie($this->cookieName, $final_value);
        $cookie->domain = $this->cookieDomain;
        $cookie->path = "/";
        if ($duration > 0)
            $cookie->expire = time() + $duration;
        Yii::app()->request->cookies[$this->cookieName] = $cookie;
        return $cookie;
    }

    public function updateCookie($user_id, $name, $email, $authFrom, $duration = 0)
    {
        $cookie_data = array(
            'id' => $user_id,
            'name' => $name,
            'email' => $email,
            'authFrom' => $authFrom,
        );
        $this->_user_data = $cookie_data;
        return $this->setCookie($cookie_data, $duration);
    }

    /**
     * This method is getting cookie
     * @return bool|array
     */
    protected function getCookie()
    {
        $cookies = Yii::app()->request->cookies;
        if (isset($cookies[$this->cookieName]) && $cookies[$this->cookieName]->value != '') {
            $value = Yii::app()->request->cookies[$this->cookieName]->value;
            if ($this->validateCookieValue($value)) {
                $a_value = explode("/", $value);
                list($ver, $data, $timestamp, $checksum) = $a_value;
                return json_decode(base64_decode($data), true);
            } else {
                return false;
            }
        } else {
            return false;
        }
    }

    /**
     * @param string $value Data that must be hashed
     * @param int $timestamp Timestamp
     * @return string
     */
    public function generateChecksum($value, $timestamp)
    {
        return sha1(md5(md5($value . $timestamp . self::SECRET_KEY)));
    }

    /**
     * Returns a value that uniquely represents the identity.
     * @return mixed a value that uniquely represents the identity (e.g. primary key value).
     */
    public function getId()
    {
        return ($id = $this->getUserData('id')) ? $id : null;
    }

    /**
     * @param mixed $value the unique identifier for the user. If null, it means the user is a guest.
     */
    public function setId($value)
    {
        $this->setUserData('id', $value);
    }

    /**
     * Returns the display name for the identity (e.g. username).
     * @return string the display name for the identity.
     */
    public function getName()
    {
        return ($name = $this->getUserData('name')) ? $name : $this->guestName;
    }

    /**
     * Sets the unique identifier for the user (e.g. username).
     * @param string $value the user name.
     * @see getName
     */
    public function setName($value)
    {
        $this->setUserData('name', $value);
    }

    /**
     * Returns the email
     * @return string email
     */
    public function getEmail()
    {
        return ($email = $this->getUserData('email')) ? $email : null;
    }

    /**
     * Sets the email.
     * @param string $value email
     * @see getEmail
     */
    public function setEmail($value)
    {
        $this->setUserData('email', $value);
    }

    /**
     * Returns a value indicating whether the user is a guest (not authenticated).
     * @return boolean whether the user is a guest (not authenticated)
     */
    public function getIsGuest()
    {
        return ($this->getCookie() !== false) ? false : true;
    }

    /**
     * Returns a value indicating whether the user is a advert (authenticated).
     * @return boolean whether the user is a advert (authenticated)
     */
    public function getIsAdvert()
    {
        if ($this->getCookie() !== false) {
            $user_model = new UserBase();
            $row = $user_model->isExistsEmail($this->getUserData('email'));
            if ($row !== false) {
                $advertInfo = new UserBase();
                if ($advertInfo->isAdvertById($this->getId()) !== false) {
                    return true;
                }
            } else {
                return false;
            }
        } else {
            return false;
        }
    }

    /**
     * Returns a value indicating where the user was authenticated from
     * (ex. From backend = 'backend', From frontend = 'frontend'
     * @return array|bool
     */
    public function getAuthFrom()
    {
        return $this->getUserData('authFrom');
    }

    /**
     * Sets the 'authFrom'
     * @param string $value
     * @see getEmail
     */
    public function setAuthFrom($value)
    {
        $this->setUserData('authFrom', $value);
    }

    /**
     * Performs access check for this user.
     * @param string $operation the name of the operation that need access check.
     * @param array $params name-value pairs that would be passed to business rules associated
     * with the tasks and roles assigned to the user.
     * @param boolean $allowCaching whether to allow caching the result of access check.
     * This parameter has been available since version 1.0.5. When this parameter
     * is true (default), if the access check of an operation was performed before,
     * its result will be directly returned when calling this method to check the same operation.
     * If this parameter is false, this method will always call {@link CAuthManager::checkAccess}
     * to obtain the up-to-date access result. Note that this caching is effective
     * only within the same request.
     * @return boolean whether the operations can be performed by this user.
     */
    public function checkAccess($operation, $params = array(), $allowCaching = true)
    {
        if ($allowCaching && $params === array() && isset($this->_access[$operation]))
            return $this->_access[$operation];
        else
            return $this->_access[$operation] = Yii::app()->getAuthManager()->checkAccess($operation, $this->getId(), $params);
    }

    /**
     * Redirects the user browser to the login page.
     * Before the redirection, the current URL (if it's not an AJAX url) will be
     * kept in {@link returnUrl} so that the user browser may be redirected back
     * to the current page after successful login. Make sure you set {@link loginUrl}
     * so that the user browser can be redirected to the specified login URL after
     * calling this method.
     * After calling this method, the current request processing will be terminated.
     */
    public function loginRequired()
    {
        $app = Yii::app();
        $request = $app->getRequest();

        if (($url = $this->loginUrl) !== null) {
            if (is_array($url)) {
                $route = isset($url[0]) ? $url[0] : $app->defaultController;
                $url = $app->createUrl($route, array_splice($url, 1));
            }
            $request->redirect($url);
        }
        else
            throw new CHttpException(403, Yii::t('yii', 'Login Required'));
    }

    /**
     * Initialize session user data
     * if $checkDB set true data will gets from data base anyway
     *
     * @param bool $checkDB
     * @return void
     */
    private function initUserSessionInfo($checkDB = true)
    {
        if (isset($_SESSION['user_info_is_premium']) && $checkDB === false)
            return;
        $premium = new User();
        $premium = $premium->Info($this->getId());
        $_SESSION['user_info_is_premium'] = ($premium['is_premium'] == 1) ? true : false;
        $_SESSION['user_info_premium_end_date'] = $_SESSION['user_info_is_premium'] ? $premium['premium_end_date'] : false;
    }

    /**
     * Destroy user session data
     *
     * @return void
     */
    public function unsetUserSessionInfo()
    {
        if (isset($_SESSION['user_info_is_premium'])) {
            unset($_SESSION['user_info_is_premium']);
            unset($_SESSION['user_info_premium_end_date']);
        }
        if(isset($_SESSION['user_']))
            unset($_SESSION['user_']);
        if(isset($_SESSION['fp']))
            unset($_SESSION['fp']);
        if (isset($_SESSION['antifrod_login_actions']))
            unset($_SESSION['antifrod_login_actions']);
    }

    /**
     * Get info is current user premium
     *
     * @return bool
     */
    public function isPremium()
    {
        $this->initUserSessionInfo(false);
        return $_SESSION['user_info_is_premium'];
    }

    /**
     * If current user premium, return end date of premium
     *
     * @return string | false
     */
    public function getPremiumEndDate()
    {
        $this->initUserSessionInfo(false);
        return $_SESSION['user_info_premium_end_date'];
    }

    /**
     * Returns all flash messages.
     * This method is similar to {@link getFlash} except that it returns all
     * currently available flash messages.
     * @param boolean $delete whether to delete the flash messages after calling this method.
     * @return array flash messages (key => message).
     * @since 1.1.3
     */
    public function getFlashes($delete=true)
    {
        $flashes=array();
        $prefix=$this->getStateKeyPrefix().self::FLASH_KEY_PREFIX;
        $keys=array_keys($_SESSION);
        $n=strlen($prefix);
        foreach($keys as $key)
        {
            if(!strncmp($key,$prefix,$n))
            {
                $flashes[substr($key,$n)]=$_SESSION[$key];
                if($delete)
                    unset($_SESSION[$key]);
            }
        }
        if($delete)
            $this->setState(self::FLASH_COUNTERS,array());
        return $flashes;
    }

    /**
     * Returns a flash message.
     * A flash message is available only in the current and the next requests.
     * @param string $key key identifying the flash message
     * @param mixed $defaultValue value to be returned if the flash message is not available.
     * @param boolean $delete whether to delete this flash message after accessing it.
     * Defaults to true. This parameter has been available since version 1.0.2.
     * @return mixed the message message
     */
    public function getFlash($key,$defaultValue=null,$delete=true)
    {
        $value=$this->getState(self::FLASH_KEY_PREFIX.$key,$defaultValue);
        if($delete)
            $this->setFlash($key,null);
        return $value;
    }

    /**
     * Stores a flash message.
     * A flash message is available only in the current and the next requests.
     * @param string $key key identifying the flash message
     * @param mixed $value flash message
     * @param mixed $defaultValue if this value is the same as the flash message, the flash message
     * will be removed. (Therefore, you can use setFlash('key',null) to remove a flash message.)
     */
    public function setFlash($key,$value,$defaultValue=null)
    {
        $this->setState(self::FLASH_KEY_PREFIX.$key,$value,$defaultValue);
        $counters=$this->getState(self::FLASH_COUNTERS,array());
        if($value===$defaultValue)
            unset($counters[$key]);
        else
            $counters[$key]=0;
        $this->setState(self::FLASH_COUNTERS,$counters,array());
    }

    /**
     * @param string $key key identifying the flash message
     * @return boolean whether the specified flash message exists
     */
    public function hasFlash($key)
    {
        return $this->getFlash($key, null, false)!==null;
    }

    /**
     * @return string a prefix for the name of the session variables storing user session data.
     */
    public function getStateKeyPrefix()
    {
        if($this->_keyPrefix!==null)
            return $this->_keyPrefix;
        else
            return $this->_keyPrefix=md5('Yii.'.get_class($this).'.'.Yii::app()->getId());
    }

    /**
     * @param string $value a prefix for the name of the session variables storing user session data.
     * @since 1.0.9
     */
    public function setStateKeyPrefix($value)
    {
        $this->_keyPrefix=$value;
    }

    /**
     * Returns the value of a variable that is stored in user session.
     *
     * This function is designed to be used by CWebUser descendant classes
     * who want to store additional user information in user session.
     * A variable, if stored in user session using {@link setState} can be
     * retrieved back using this function.
     *
     * @param string $key variable name
     * @param mixed $defaultValue default value
     * @return mixed the value of the variable. If it doesn't exist in the session,
     * the provided default value will be returned
     * @see setState
     */
    public function getState($key,$defaultValue=null)
    {
        $key=$this->getStateKeyPrefix().$key;
        return isset($_SESSION[$key]) ? $_SESSION[$key] : $defaultValue;
    }

    /**
     * Stores a variable in user session.
     *
     * This function is designed to be used by CWebUser descendant classes
     * who want to store additional user information in user session.
     * By storing a variable using this function, the variable may be retrieved
     * back later using {@link getState}. The variable will be persistent
     * across page requests during a user session.
     *
     * @param string $key variable name
     * @param mixed $value variable value
     * @param mixed $defaultValue default value. If $value===$defaultValue, the variable will be
     * removed from the session
     * @see getState
     */
    public function setState($key,$value,$defaultValue=null)
    {
        $key=$this->getStateKeyPrefix().$key;
        if($value===$defaultValue)
            unset($_SESSION[$key]);
        else
            $_SESSION[$key]=$value;
    }

    /**
     * Returns a value indicating whether there is a state of the specified name.
     * @param string $key state name
     * @return boolean whether there is a state of the specified name.
     * @since 1.0.3
     */
    public function hasState($key)
    {
        $key=$this->getStateKeyPrefix().$key;
        return isset($_SESSION[$key]);
    }

    /**
     * Clears all user identity information from persistent storage.
     * This will remove the data stored via {@link setState}.
     */
    public function clearStates()
    {
        $keys=array_keys($_SESSION);
        $prefix=$this->getStateKeyPrefix();
        $n=strlen($prefix);
        foreach($keys as $key)
        {
            if(!strncmp($key,$prefix,$n))
                unset($_SESSION[$key]);
        }
    }
}
